<?php
if(!defined('X2'))  die("Direct inclusion of ". __FILE__);

require_once(X2 . "/classes/request/request.class.php");
require_once(X2 . "/classes/myDB/myDB.class.php");
require_once(X2 . "/classes/user/user2.class.php");

class x2 {

    var $ext;
    var $serverName;

    var $uri = "";
    var $method = "GET";

    var $ancestors = array();
    var $tree;
    var $treeXPath;
    var $templates;
    var $templatesXPath;
    var $contentTypes = array();
    var $handlers = array();

    var $xslt;
    var $doc;
    var $headers = array();

    var $modified = array();
    var $body = "";

    var $user;
    var $db;
    var $request;

    var $debug = false;
    var $prefix = 'x2_';

    function x2(){
        $this -> db =& myDB::getConnection2(
            'mysql://'. X2_SQL_USER .':'. X2_SQL_PASS .'@'. X2_SQL_HOST .'/'. X2_SQL_DB
        );
        if($this -> db === false) $this -> unavailable();
        $this -> addHeader("Content-type", "text/html; charset=UTF-8");
        $this -> addHeader("Cache-Control", "private, proxy-revalidate");

        /* Обработчики */
        $globalExt = include(X2 . '/ext/index.inc.php');
        foreach($globalExt as $id => $src){
            $globalExt[$id] = realpath(X2 ."/ext/". $src);
        }
        $this -> handlers = $globalExt;
        $this -> serverName = $_SERVER['HTTP_HOST'];

        // Пользователь
        $this -> user =& User::getInstance();

        // Типы содержимого
        $this -> contentTypes = $this -> db -> arrayQuery(
            "SELECT * FROM `x2_types`",
            "id"
        );
    }

    function &getInstance(){
        static $instance;
        if(!isset($instance)){
            $instance = new x2;
        }
        return $instance;
    }

    function locate($url = "/", $method = "GET"){
  	    return $this -> parseRequest($url, $method);
    }

    function parseRequest($url = "/", $method = "GET"){
        $this -> request = new Request($url, $method);
        $this -> ancestors = $this -> mapURLToTree($this -> request -> steps);
        if(is_array($this -> ancestors)){
            $l = end($this -> ancestors);
            if($l['url'] != $this -> request -> rPath) return false;
        }
        // Разобрали uri
        $this -> uri = $this -> request -> rPath;
        return $this -> ancestors !== false;
    }

    function mapURLToTree($steps){
        $this -> steps = array();
        $s = array();
        for($i = 0; $i < count($steps); $i++){
            $this -> steps[$i] = join("/", array_slice($steps, 0, $i + 1));
        	$s[$i] = $this -> db -> quote($this -> steps[$i]);
        }
        return $this -> db -> arrayQuery(
            "SELECT * FROM `x2_tree`
            WHERE url IN ('". join("','", $s) ."')
            ORDER BY FIND_IN_SET(url, '". join(",", $s) ."')"
        );
    }

    function buildTree () {
        if($this -> debug) $begin = getmicrotime();
        $xmlStr = "<!DOCTYPE x:doc [\n". file_get_contents(X2. "/xsl/symbols.ent") ."]>\n";
        foreach($this -> ancestors as $node){
            $xmlStr .= '<x:doc xmlns:x="'. X2_NS .'"';
            $xmlStr .= ' xmlns="http://www.w3.org/1999/xhtml"';
            $xmlStr .= ' id="'. $node['id'] .'"';
            $xmlStr .= ' uri="'. $node['url'] .'"';
            $xmlStr .= ' mtime="'. $node['mtime'] .'"';
            $xmlStr .= ' ctime="'. $node['ctime'] .'"';
            $xmlStr .= ' state="'. $node['state'] .'"';
            $xmlStr .= ' type="'. $node['type'] .'"';
            $xmlStr .= ' position="'. $node['position'] .'"';
            $xmlStr .= ' visible="'. $node['visible'] .'"';
            $xmlStr .= '>';
            $xmlStr .= '<x:title>'. $node['title'] .'</x:title>';
            $xmlStr .= '<x:description>'. $node['description'] .'</x:description>';
            $xmlStr .= '<x:keywords>'. $node['keywords'] .'</x:keywords>';
            $xmlStr .= '<x:thumbnail>'. $node['thumbnail'] .'</x:thumbnail>';
        }
        $xmlStr .= '<x:content-types>';
        foreach($this -> contentTypes as $type){
            $xmlStr .= '<x:type id="'. $type['id'] .'" name="'. $type['name'] .'"/>';
        }
        $xmlStr .= '</x:content-types>';
        foreach($this -> ancestors as $node){
            $xmlStr .= '</x:doc>';
        }
        $this -> tree = domxml_open_mem(
            $xmlStr,
            DOMXML_LOAD_RECOVERING | DOMXML_LOAD_DONT_KEEP_BLANKS
        );

        $this -> treeXPath = $this -> tree -> xpath_new_context();
        $this -> treeXPath -> xpath_register_ns('x', X2_NS);
        $this -> treeXPath -> xpath_register_ns('xhtml', 'http://www.w3.org/1999/xhtml');
        $sel = $this -> treeXPath -> xpath_eval("//x:doc[not(x:doc)]");
        $this -> doc = $sel -> nodeset[0];
        $this -> docId = $this -> doc -> get_attribute('id');

        if($this -> debug){
          $report = "Content building: " . (getmicrotime() - $begin);
          if($this -> _memgetus) $report .= " [Memory: " . memory_get_usage() . " b]";
          $this -> profiler[] = $report;
        }
    }

    function getContext(){
        $url = "";
        $snippets = array();
        foreach($this -> request -> steps as $step){
        	if($step != '') $url .= "/" . $step;
            $data = $this -> db -> arrayQuery(
                "SELECT s.* FROM `x2_snippets` AS s, `x2_tree` AS t
                WHERE
                    t.url='". $this -> db -> quote($url) ."' AND
                    t.id=s.docId AND
                    s.type='context'",
                    "fieldId"
            );
            if($data) {
                $snippets = array_merge($snippets, $data);
            }
        }
//        exit(var_export($this, 1));
        $xmlStr = "<!DOCTYPE x:block [\n". file_get_contents(X2. "/xsl/symbols.ent") ."]>\n";
        $xmlStr .= '<x:block id="context" xmlns:x="http://www.loukianov.com/x2" xmlns="http://www.w3.org/1999/xhtml">';
        foreach($snippets as $name => $field){
            $xmlStr .= '<x:field id="'. $name .'" editable="'. $field['editable'] .'" srcId="'. $field['docId'] .'">';
            $xmlStr .= $field['markup'];
            $xmlStr .= '</x:field>';
        }
        $xmlStr .= '</x:block>';

        $dom = domxml_open_mem(
            $xmlStr,
            DOMXML_LOAD_RECOVERING | DOMXML_LOAD_DONT_KEEP_BLANKS
        );
        $clone = $dom -> document_element();
        $this -> doc -> append_child(
            $clone -> clone_node(true)
        );
    }

    function getContent(){
    	$data = array();
        $names = trim($this -> contentTypes[$this -> doc -> get_attribute('type')]['fields']);
        $default = $this -> db -> arrayQuery(
            "SELECT * FROM `x2_fields`
            WHERE FIND_IN_SET(name, '". $this -> db -> quote($names) ."') > 0",
            "name"
        );

        foreach($default as $n => $v){
            $default[$n]['docId'] = $this -> docId;
            $default[$n]['fieldId'] = $n;
            $default[$n]['text'] = strip_tags($default[$n]['markup']);
        }

        $f = explode(
            ",",
            $names
        );
        foreach($f as $v){
       	    $v = trim($v);
            $data[$v] = array(
                'docId' => $this -> docId,
                'fieldId' => $v,
                'editable' => 1,
                'markup' => '',
                'text' => ''
            );
        }
        $data = array_merge(
            $data,
            $default,
            $this -> db -> arrayQuery(
                "SELECT * FROM `x2_snippets`
                WHERE docId={$this -> docId} AND type='content'",
                "fieldId"
            )
        );

        $xmlStr = "<!DOCTYPE x:block [\n". file_get_contents(X2. "/xsl/symbols.ent") ."]>\n";
        $xmlStr .= '<x:block id="content" xmlns:x="http://www.loukianov.com/x2" xmlns="http://www.w3.org/1999/xhtml">';
        foreach($data as $field){
            $xmlStr .= '<x:field id="'. $field['fieldId'] .'" editable="'. $field['editable']. '">';
            $xmlStr .= $field['markup'];
            $xmlStr .= '</x:field>';
        }
        $xmlStr .= '</x:block>';

        $dom = domxml_open_mem(
            $xmlStr,
            DOMXML_LOAD_RECOVERING | DOMXML_LOAD_DONT_KEEP_BLANKS | DOMXML_LOAD_SUBSTITUTE_ENTITIES
        );
        $clone = $dom -> document_element();
        $this -> doc -> append_child(
            $clone -> clone_node(true)
        );
    }

    function process($url = "/", $method = "GET") {
        $begin = getmicrotime();
        if($this -> locate($url) || $this -> locate("/404")){
            $this -> buildTree();
            $this -> appendBlock("GET", $this -> request -> get);
            $this -> appendBlock("POST", $this -> request -> post);
            $this -> appendBlock("COOKIE", $this -> request -> cookie);
            $this -> appendBlock("REQUEST", $this -> request -> request);
            $this -> appendUserInfo();
            $this -> getContext();
            $this -> getContent();
            $this -> execHandlers();

            if(isset($_GET['xslt'])) {
       	        $this -> sendXML();
            } else {
                $this -> getTemplates();
                $this -> transform();
            }
        } else {
            $this -> notFound();
        }
//        exit(var_export($this, 1));
        $this -> output($begin);
    }

    function execHandlers(){
    /* Запуск обработчиков, содержащихся непосредственно в XML
     * и в массиве $this -> handlers.
     * Производится сразу после построения начального DOM-дерева,
     * содержащего контент и контекст
    **/
/*        if($this -> debug) $begin = getmicrotime();*/
        $sel = $this -> treeXPath -> xpath_eval(
            "//x:handler"
        );
        foreach($sel -> nodeset as $handler){
            $this -> loadHandler($handler);
        }
/*        if($this -> debug){
            $report = "Handlers processing ('onload'): ". (getmicrotime() - $begin);
            if($this -> _memgetus) $report .= " [Memory: " . memory_get_usage() . " b]";
          $this -> profiler[] = $report;
        }*/
    }

    function loadHandler($handler){
    /* Находим узел, к которому привязан обработчик - это либо x:doc,
     * либо x:block
    **/
        $params = array();
        $block = $handler -> parent_node();
        // Сначала выберем общие параметры для всех обработчиков, если такие есть
        foreach($block -> child_nodes() as $p){
            if($p -> node_name() == 'param'){
                $params[$p -> get_attribute("name")] = $p -> get_attribute("value");
            }
        }
        $src = $this -> handlers[$handler -> get_attribute("id")];
        if(file_exists($src)){
            foreach ($handler -> child_nodes() as $p){
                if($p -> node_name() == 'param'){
                    $params[$p -> get_attribute("name")] = $p -> get_attribute("value");
                }
            }
            include($src);
            if($block -> has_attribute('xsl')){
                $xsltSrc =  $block -> get_attribute('xsl');
                if($xsltSrc != '' && file_exists($xsltSrc)){
                // можно обработать шаблоном
                }
            }
        }
    /* ТуДу::
     * Если в процессе обработки было создано поддерево, содержащее новые
     * обработчики, надо бы и их обработать.
     * Правда, новое поддерево теоретически можно воткнуть в любое место
     * исходного и определить, какие обработчики уже отработали, а какие нет,
     * можно только как-то помечая отработанные... Биляттт. Надо будет обдумать.
    **/
    }

    function getTemplates(){
        $this -> xsltSrc = array();
        $src = X2 ."/layout/__default.xsl";
        if(file_exists($src)){
            $this -> xsltSrc[] = $src;
            $this -> modified[] = filemtime($src);
        }
        $src = X2_PROJECT ."/layout/default.xsl";
        if(file_exists($src)){
            $this -> xsltSrc[] = $src;
            $this -> modified[] = filemtime($src);
        }
    }

    function transform(){
	    $genericSrc = '<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
 xmlns:x="http://www.loukianov.com/x2"
 xmlns:xhtml="http://www.w3.org/1999/xhtml"
 xmlns="http://www.w3.org/1999/xhtml"
 exclude-result-prefixes="xsl x xhtml">';
        foreach($this -> xsltSrc as $src){
          $genericSrc .= '<xsl:import href="'. localURLFix($src) .'"/>';
        }

        if($this -> user -> userInfo['status'] == 4){
            $genericSrc .= '<xsl:include href="'. localURLFix(X2 ."/xsl/x2-toolbar.xsl") .'"/>';
        }
$genericSrc .= '
<xsl:param name="mode">view</xsl:param>
<xsl:param name="year">'. date("Y") .'</xsl:param>
<xsl:param name="serverName">'. "http://" . $this -> serverName .'</xsl:param>
<xsl:param name="current-id">'. $this -> doc -> get_attribute('id'). '</xsl:param>

</xsl:stylesheet>';

        $this -> xslt = domxml_xslt_stylesheet($genericSrc);
        $this -> body = $this -> xslt -> result_dump_mem(
          $this -> xslt -> process($this -> tree)
        );
    }

    function output($begin){
      	// Перехватываем вывод
      	ob_start();
        eval('?>'. $this -> body .'<?php ');
        $this -> body = ob_get_clean();

      	$inclusions = get_included_files();
        foreach($inclusions as $fname){
          $this -> modified[] = filemtime($fname);
        }
        $modified = max($this -> modified);

        $eTag = '"'. md5($this -> body) .'"';

        if($this -> request -> ifModifiedSince >= $modified && $this -> request -> ifNoneMatch == $eTag){
            // Содержимое не изменялось
            header("HTTP/1.1 304 Not Modified");
            header("ETag: $eTag");
            header("Cache-Control: private, must-revalidate, max-age=0");
            return false;
        } else {
            // Содержимое изменилось
            $this -> addHeader(
                "Last-Modified",
                gmdate("D, d M Y H:i:s \G\M\T", $modified)
            );
            $this -> addHeader("ETag", $eTag);
            $this -> addHeader(
                "Cache-Control",
                "private, must-revalidate, max-age=0"
            );
            if(!(bool)ini_get("zlib.output_compression")){
                $this -> addHeader("Content-Length", strlen($this -> body));
            }

            $this -> addHeader("X-PGT", getmicrotime() - $begin);

            foreach ($this -> headers as $name => $set){
                foreach ($set as $value){
                    if($value != '') header ("$name: $value");
                }
            }
            if(strtoupper($this -> method) != 'HEAD') echo $this -> body;
        }
    }

    /* misc methods */
    function addHeader($name, $value, $bOverwrite = true){
        if($bOverwrite) {
            $this -> headers [$name] = array($value);
        } else {
            $this -> headers [$name][] = $value;
        }
    }

    function appendUserInfo(){
  	    $dom = domxml_open_mem($this -> user -> toString());
  	    $domRoot = $dom -> document_element();
  	    $this -> doc -> append_child(
  	        $domRoot -> clone_node(true)
  	    );
    }

    function appendBlock ($className, $data = array()){
        if(count($data) > 0){
            $b = $this -> doc -> append_child(
                $this -> tree -> create_element_ns(X2_NS, 'block', 'x')
            );
            $b -> set_attribute('class', $className);
            foreach($data as $k => $v){
                $f = $b -> append_child(
                    $this -> tree -> create_element_ns(X2_NS, 'field', 'x')
                );
                $f -> set_attribute('name', $k);
                $f -> append_child(
                    $this -> tree -> create_text_node(
                        toUTF8($v)
                    )
                );
            }
        }
    }

    function getModule($moduleName){
        ob_start();
        if(isset($this -> modules[$moduleName]) && file_exists($this -> ext .'/'. $this -> modules[$moduleName])){
            include_once($this -> ext .'/'. $this -> modules[$moduleName]);
        } else {
            echo "<!-- component $moduleName is not available -->";
        }

        return ob_get_clean();
    }

    function sendXML() {
        header("Content-type: text/xml; charset=UTF-8");
        exit($this -> tree -> dump_mem(1, "UTF-8"));
    }

    function redirect($location = "/"){
        $body = "";
        header("HTTP/1.1 302 Found");
        header("Location: http://". $this -> serverName . $location);
        if($this -> method != 'HEAD'){
            $body = '<html><head><title>Redirection</title></head><body><a href="'. $location .'">'. $location .'</a></body></html>';
        }
        exit($body);
    }

    function notFound(){
        header("HTTP/1.1 404 Not Found");
        header("Content-type: text/html; charset=UTF-8");
        $body = "<html>\n";
        $body .= "<head><title>404 Документ не найден</title></head>\n";
        $body .= "<body>\n";
        $body .= "<h1>Документ не&nbsp;найден</h1>\n";
        $body .= "<p>Запрошенный документ не был найден на сервере. Возможно он был перемещен или удален.</p>\n";
        $body .= str_repeat("<!-- comment for IE -->\n", 1000);
        $body .= "</body>\n";
        $body .= "</html>";
        header("Content-length: ". strlen($body));
        exit($body);
    }

    function unavailable(){
        header("HTTP/1.1 503 Service Unavailable");
        header("Content-type: text/html; charset=UTF-8");
        $body = "<html>\n";
        $body .= "<head><title>503 Сервис временно недоступен</title></head>\n";
        $body .= "<body>\n";
        $body .= "<h1>Сервис временно недоступен</h1>\n";
        $body .= "<p>По техническим причинам сервер не смог обработать запрос. Попробуйте обновить страницу или зайдите позже.</p>\n";
        $body .= str_repeat("<!-- comment for IE -->\n", 1000);
        $body .= "</body>\n";
        $body .= "</html>";
        header("Content-length: ". strlen($body));
        exit($body);
    }

    function incompatible(){
        header("HTTP/1.1 503 Service Unavailable");
        header("Content-type: text/html; charset=UTF-8");
        $body = "<html>\n";
        $body .= "<head><title>503 Сервис временно недоступен</title></head>\n";
        $body .= "<body>\n";
        $body .= "<h1>Отсутствуют необходимые компоненты</h1>\n";
        $body .= "<p>На сервере отсутствуют необходимые для работы компоненты.</p>\n";
        $body .= str_repeat("<!-- comment for IE -->\n", 1000);
        $body .= "</body>\n";
        $body .= "</html>";
        header("Content-length: ". strlen($body));
        exit($body);
    }

/* data manipulation will be moved to extended class */

    function getChildren($url){
    	$quotedURL = $this -> db -> quote($url);
        return $this -> db -> arrayQuery(
            "SELECT * FROM `x2_tree`
            WHERE LEFT(url, LENGTH('{$quotedURL}/')) = '{$quotedURL}/' AND
            LOCATE('/', url, LENGTH('{$quotedURL}/') + 1) = 0"
        );
    }

    function getMaxPosition($url){
    	$quotedURL = $this -> db -> quote($url);
        return $this -> db -> getSingle(
            "SELECT IFNULL(MAX(position) + 1, 0) AS last FROM `x2_tree`
            WHERE LEFT(url, LENGTH('{$quotedURL}/')) = '{$quotedURL}/' AND
            LOCATE('/', url, LENGTH('{$quotedURL}/') + 1) = 0",
            "last"
        );
    }

    function urlExists($url){
    	return $this -> db -> getSingle(
    	    "SELECT COUNT(*) AS c WHERE url = '". $this -> db -> quote($url) ."'",
    	    "c"
    	);
    }

    function createNode($data){
        $t = time();
        $a = array_merge(
            array(
                'parent' => '',
                'url' => '',
                'position' => 0,
                'title' => 'Документ',
                'keywords' => '',
                'description' => '',
                'thumbnail' => '',
                'type' => 1,
                'ctime' => $t,
                'mtime' => $t,
                'state' => 'published',
                'visible' => 'no'
            ),
            $data
        );

        if($data['url'] == '') return false;

        $this -> db -> query("LOCK TABLES `x2_tree` WRITE");

        $pos = $this -> getMaxPosition($data['parent']);

        $this -> db -> query(
            "INSERT INTO `x2_tree`
            (url, position, title, keywords, description, thumbnail, type, ctime, mtime, state, visible)
            VALUES (
                '". $this -> db -> quote($a['url']) ."',
                {$pos},
                '". $this -> db -> quote($a['title']) ."',
                '". $this -> db -> quote($a['keywords']) ."',
                '". $this -> db -> quote($a['description']) ."',
                '". $this -> db -> quote($a['thumbnail']) ."',
                '". intval($a['type']). "',
                '". intval($a['ctime']). "',
                '". intval($a['mtime']). "',
                '". $this -> db -> quote($a['state']) ."',
                '". $this -> db -> quote($a['visible']) ."'
            )"
        );
        $newId = $this -> db -> lastId();
        $this -> db -> query("UNLOCK TABLES");
        return $newId > 0;
    }

    function updateNode($data){
        $t = time();
        $a = array_merge(
            array(
                'url' => '',
                'title' => 'Документ',
                'keywords' => '',
                'description' => '',
                'thumbnail' => '',
                'mtime' => $t,
                'state' => 'published',
                'visible' => 'no'
            ),
            $data
        );

        $this -> db -> query(
            "UPDATE `x2_tree`
            SET
                title = '". $this -> db -> quote($a['title']) ."',
                keywords = '". $this -> db -> quote($a['keywords']) ."',
                description = '". $this -> db -> quote($a['description']) ."',
                thumbnail = '". $this -> db -> quote($a['thumbnail']) ."',
                mtime = '". intval($a['mtime']). "',
                state = '". $this -> db -> quote($a['state']) ."',
                visible = '". $this -> db -> quote($a['visible']) ."'
            WHERE
                url = '". $this -> db -> quote($a['url']) ."'"
        );
        return $this -> db -> wasChanged() > 0;
    }

    function removeNode($url){
        $ids = $this -> db -> arrayQuery(
            "SELECT id FROM `x2_tree`
            WHERE url = '". $this -> db -> quote($url) ."' OR
                url LIKE '". $this -> db -> quote($url) ."/%'",
                "id"
        );
        $idStr = join(array_keys($ids), ",");
        $this -> db -> query(
            "DELETE FROM `x2_tree` WHERE id IN ({$idStr})"
        );
        $this -> db -> query(
            "DELETE FROM ` x2_snippets` WHERE docId IN ({$idStr})"
        );
        return count($ids);
    }

    function saveContent($docId, $data){// в очередь, сукины дети, в очередь!
        $this -> db -> query(
            "REPLACE INTO `x2_snippets` (docId, fieldId, type, markup, text)
            VALUES (
                '". intval($docId) ."',
                '". $this -> db -> quote($data['name']) ."',
                'content',
                '". $this -> db -> quote($data['markup']) ."',
                '". $this -> db -> quote(strip_tags($data['markup'])) ."'
            )"
        );
        return true;
    }

    function moveNodeUp($url){
        return true;
    }

    function moveNodeDown($url){
        return true;
    }

    function setState($docId, $state){
    	$a = array('draft', 'published', 'archive', 'removed');
    	if(!in_array($state, $a)) return false;
        $this -> db -> query(
            "UPDATE `x2_tree`
            SET state='". $this -> db -> quote($state) ."'
            WHERE id=". intval($docId)
        );
        return $this -> db -> wasChanged() > 0;
    }
}
?>